查看原文
其他

Flutter混编工程之异常处理

徐宜生 群英传
2024-08-24

点击上方蓝字关注我,知识会给你力量


Flutter App层和Framework层的异常,通常是不会引起Crash的,但是Engine层的异常会造成Crash。而Flutter Engine部分的异常,主要是libfutter.so发生的异常,这部分的异常,在Dart层无法捕获,一般会交给类似Bugly这样的平台来收集。

我们能主动监控的,主要是Dart层的异常,这些异常虽然不会让App crash,但是统计这些异常对于提高我们的用户体验,是非常有必要的。

同步异常与异步异常

对于同步异常来说,直接使用try-catch就可以捕获异常,如果要指定捕获的异常类型,可以使用on关键字。但是,try-catch不能捕获异步异常,就像下面的代码,是无法捕获的。

try {
  Future.error("error");
catch (e){
  print(e)
}

这和在Java中,try-catch捕获Thread中的异常类似,对于异步异常来说,只能使用Future的catchError或者是onError来捕获异常,代码如下所示。

Future.delayed(Duration(seconds: 1)).then((value) => print(value), onError: (e) {});

Dart的执行队列是一个单线程模型,所以在事件循环队列中,当某个Task发生异常并没有被捕获时,程序并不会退出,只是当前的Task异常中止,也就是说一个Task发生的异常是不会影响其它Task执行的。

Widget Build异常

Widget在Build过程中如果发生异常,例如在build函数中出错(throw exception),我们会看见一个深红色的异常界面,这个就是Flutter自带的异常处理界面,我们来看下源代码中,Flutter对这类异常的处理方式。在ComponentElement的实现中,我们找到performRebuild函数,这个是函数是build时所调用的,我们在这里,可以找到相关的实现。

如下所示,在执行到build()函数如果出错时,就会被catch,从而创建一个ErrorWidget。

再进入_debugReportException中一探究竟,你会发现,应用层的异常被catch之后,都是通过FlutterError.reportError来处理的。

在reportError中,会调用onError来处理,默认的处理方式是dumpErrorToConsole,它就是onError的默认实现。

在这里我们还能发现如何判断debug模式,看源码是不是很有意思。

通过上面的源码,我们就可以了解到,当Flutter应用层崩溃后,SDK的处理,简而言之,就是会构建一个错误界面,同时回调onError函数。在这里,我们可以通过修改这个静态的回调函数,来创建自己的处理方式。

所以,很简单,我们只需要在main()中,执行下面的代码即可。


var defaultError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
  defaultError?.call(details);// 根据需要是否要保留default处理
  reportException(details);
};

defaultError?.call(details)就是默认将异常日志打印到console的方法,如果不用,这里可以去掉。

重写错误界面

前面我们看到了,在源代码中,Flutter自定义了一个ErrorWidget作为默认的异常界面,在平时的开发中,我们可以自定义ErrorWidget.builder,实现一个更友好的错误界面,例如封装一个统一的异常提示界面。

ErrorWidget.builder = (FlutterErrorDetails details) {
  return MaterialApp(
    theme: ThemeData(primarySwatch: Colors.red),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('出错了,请稍后再试'),
      ),
      body: SingleChildScrollView(
        child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(details.toString()), // 后续修改为统一的错误页
          )),
    ),
  );
};

如上所示,通过修改ErrorWidget.builder,就可以将任意自定义的界面作为异常界面了。

全局未捕获异常

前面讲到的,都是属于被捕获的异常,而有一些异常,在代码中是没有被捕获的,这就类似Android的UncaughtExceptionHandler,Flutter也提供了一个全局的异常处理钩子函数,所有的未捕获异常,无论是同步异常还是异步异常,都会在这里被监听。

在Dart中,SDK提供了一个Zone的概念,一个Zone就类似一个沙箱,在Zone里面,可以拥有独立的异常处理、print函数等等功能,多个Zone之间是彼此独立的,所以,我们只需要将App运行在一个Zone里面,就可以借助它的handleUncaughtError来处理所有的未捕获异常了。下面是使用Zone的一个简单示例。

void main() {
  runZoned(
    () => runApp(const MyApp(color: Colors.blue)),
    zoneSpecification: ZoneSpecification(
      handleUncaughtError: (
        Zone self,
        ZoneDelegate parent,
        Zone zone,
        Object error,
        StackTrace stackTrace,
      ) {
        reportException(
          FlutterErrorDetails(
            exception: error,
            stack: stackTrace,
          ),
        );
      },
    ),
  );
}

根据文档中的提升,可以使用runZonedGuarded来进行简化,代码如下所示。

void main() {
  runZonedGuarded(
    () => runApp(const MyApp(color: Colors.blue)),
    (Object error, StackTrace stack) {
      reportException(
        FlutterErrorDetails(
          exception: error,
          stack: stack,
        ),
      );
    },
  );
}

封装

下面我们将前面的异常处理方式都合并到一起,并针对EngineGroup的多入口处理,封装一个类,代码如下所示。

class SafeApp {
  run(Widget app) {
    ErrorWidget.builder = (FlutterErrorDetails details) {
      return MaterialApp(
        theme: ThemeData(primarySwatch: Colors.red),
        home: Scaffold(
          appBar: AppBar(
            title: const Text('出错了,请稍后再试'),
          ),
          body: SingleChildScrollView(
            child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(details.toString()), // 后续修改为统一的错误页
              )),
        ),
      );
    };
    FlutterError.onError = (FlutterErrorDetails details) {
      Zone.current.handleUncaughtError(details.exception, details.stack!);
    };
    runZonedGuarded(
      () => runApp(const MyApp(color: Colors.blue)),
      (Object error, StackTrace stack) {
        reportException(
          FlutterErrorDetails(
            exception: error,
            stack: stack,
          ),
        );
      },
    );
  }
}

在这里,我们构建了下面这些异常处理的方式:

  • 统一的异常处理界面
  • 将Build异常统一转发到Zone中的异常处理函数来进行处理
  • 将所有的未捕获异常记录

这样的话,我们在使用时,只需要对原始的App进行下调用即可。

void main() => SafeApp().run(const MyApp(color: Colors.blue));

这样就完成了异常处理的封装。

上报

在Flutter侧,我们只是获取了异常的相关信息,如果需要上报,那么我们需要借助Channel,桥接的Native,使用Bugly或其它平台进行上报,我们可以借助Pigeon来进行处理,还不熟悉的朋友可以参考我前面的文章。
Flutter混编工程之高速公路Pigeon
Flutter混编工程之通讯之路
通过Channel,我们可以把异常数据报给Native侧,再让Native侧走自己的上报通道,例如Bugly等。

NativeCommonApi().reportException('------Flutter_Exception------\n${details.exceptionAsString()}\n${details.stack.toString()}');

同时,Flutter提供了exceptionAsString()方法,将异常信息展示的更加友好一点,我们可以借助它来做一些格式化的操作。

3.3版本API的改进

官方的API更新如下:
https://docs.flutter.dev/testing/errors
PlatformDispatcher.onError在以前的版本中,开发者必须手动配置自定义Zone才能捕获应用程序的所有异常和错误,但是自定义Zone对Dart核心库中的一些优化是有害的,这会减慢应用程序的启动时间。「在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。」

所以,3.3之后,我们不用再设置Zone来捕获全局异常了,只用设置PlatformDispatcher.instance.onError即可。

import 'package:flutter/material.dart';
import 'dart:ui';

Future<void> main() async {
  await myErrorsHandler.initialize();
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    myErrorsHandler.onErrorDetails(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    myErrorsHandler.onError(error, stack);
    return true;
  };
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != nullreturn widget;
        throw ('widget is null');
      },
    );
  }
}

向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


继续滑动看下一个
群英传
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存